﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Windows.Data;
using GA.Server.Api;
using GA.Server.Configuration;
using GA.Server.ConnectionListener;
using GA.Server.Objects;
using GA.Shared.Objects;
using GA.Shared.Interfaces;
using GA.Shared.Exceptions;
using GA.Server.DatabaseManager;

namespace GA.Server.Managers
{
    public class UserManagerServer : GAServer, IObjectServer
    {
        /// <summary>
        /// Gets the name of the server.
        /// </summary>
        public override string Name
        {
            get { return GetType().Name; }
        }

        public byte[] Key { get; private set; }

        public UserManagerServer()
        {
        }

        public UserManagerServer(ServerPropertiesElementCollection properties)
            : base(properties)
        {
            Log.Info("Starting UserManager...");
            if (properties.Get("UserIV") == null || properties.Get("UserIV").Value.Length == 0) throw new InvalidOperationException("UserIV has not been specified. Cannot load encrypted user information without an IV.");
            if (properties.Get("UserKey") == null || properties.Get("UserKey").Value.Length == 0) throw new InvalidOperationException("UserKey has not been specified. Cannot load encrypted user information without a Key.");

            RijndaelManaged cryptographer = new RijndaelManaged();

            if (properties.Get("CryptKey") != null)
            {
                Key = ConvertStringToByteArray(properties.Get("CryptKey").Value);
            }

            if (Key == null)
            {
                cryptographer.GenerateKey();
                Key = cryptographer.Key;
            }
            UserIV = ConvertStringToByteArray(properties.Get("UserIV").Value);
            UserKey = ConvertStringToByteArray(properties.Get("UserKey").Value);
            mIVUsers = new Dictionary<IPAddress, byte[]>();
        }

        public override void RegisterServer(GAServer newServer)
        {
            if (newServer is ConnectionListenerServer)
            {
                Log.Info("Registering Server with ConnectionListenerServer...");
                ConnectionListenerServer connectionListenerServer = (ConnectionListenerServer)newServer;
                connectionListenerServer.AuthenticateUser = new Func<Authentication, User>((authentication) =>
                {
                    DatabaseUser authenicateUser = AuthenticateDatabaseUser(authentication);
                    if (authenicateUser == null)
                    {
                        return null;
                    }
                    else
                    {
                        return new User(authenicateUser, BindingMode.OneWay);
                    }
                });
                connectionListenerServer.GetIVFunc = new Func<IPAddress, byte[]>((ipAddress) => { return GetIV(ipAddress); });
                connectionListenerServer.GetKeyFunc = new Func<byte[]>(() => { return Key; });
                connectionListenerServer.RegisterUser = new Func<Authentication, User>((authentication) =>
                {
                    DatabaseUser newUser = CreateDatabaseUser(authentication);
                    if (newUser == null)
                    {
                        return null;
                    }
                    else
                    {
                        return new User(newUser, BindingMode.OneWay);
                    }
                });
                connectionListenerServer.RegisterObjectServer<User>(this);
            }
            else if (newServer is DatabaseManagerServer)
            {
                mDatabaseManagerServer = (DatabaseManagerServer)newServer;
            }
        }

        /// <summary>
        /// Gets the real user in the server from an incoming user.
        /// </summary>
        /// <param name="incomingUser">User incoming from the client.</param>
        /// <returns>User if authentication passed.</returns>
        /// <exception cref="ArgumentNullException">If incomingUser, or the authenticationKey inside it is null.</exception>
        /// <exception cref="AuthenticationException">If user does not exist, has not signed in yet, or the keys did not match.</exception>
        public DatabaseUser GetRealUser(User incomingUser)
        {
            if (incomingUser == null) throw new ArgumentNullException("incomingUser");
            if (incomingUser.AuthenticationKey == null) throw new ArgumentNullException("incomingUser.AuthenticationKey");

            DatabaseUser possibleUser = GetDatabaseUser(incomingUser.Id);
            if (possibleUser == null)
            {
                throw new AuthenticationException("User does not exist.");
            }

            if (possibleUser.AuthenticationKey == null)
            {
                throw new AuthenticationException("User has not signed in yet.");
            }
            if (!KeysMatch(incomingUser.AuthenticationKey, possibleUser.AuthenticationKey))
            {
                throw new AuthenticationException(incomingUser);
            }
            return possibleUser;
        }

        public byte[] GetIV(IPAddress ipAddress)
        {
            RijndaelManaged cryptographer = new RijndaelManaged();
            cryptographer.Key = Key;
            cryptographer.GenerateIV();
            mIVUsers.Add(ipAddress, cryptographer.IV);
            return cryptographer.IV;
        }

        public User GetUser(int id)
        {
            DatabaseUser baseUser = GetDatabaseUser(id);
            if (baseUser != null)
            {
                return new User(baseUser);
            }
            return null;
        }

        public User GetUser(string userName)
        {
            DatabaseUser baseUser = GetDatabaseUser(userName);
            if (baseUser != null)
            {
                return new User(baseUser);
            }
            return null;
        }

        public User Authenticate(Authentication authentication)
        {
            DatabaseUser baseUser = AuthenticateDatabaseUser(authentication);
            if (baseUser != null)
            {
                return new User(baseUser);
            }
            return null;
        }

        public User CreateUser(Authentication authentication)
        {
            DatabaseUser baseUser = CreateDatabaseUser(authentication);
            if (baseUser != null)
            {
                return new User(baseUser);
            }
            return null;
        }

        private byte[] UserIV { get; set; }
        private byte[] UserKey { get; set; }
        private Dictionary<int, DatabaseUser> mUsers = new Dictionary<int, DatabaseUser>();
        private Dictionary<IPAddress, byte[]> mIVUsers = new Dictionary<IPAddress, byte[]>();
        private DatabaseManagerServer mDatabaseManagerServer;

        public DatabaseUser GetDatabaseUser(User baseUser)
        {
            return GetDatabaseUser(baseUser.Id);
        }

        public DatabaseUser GetDatabaseUser(int id)
        {
            if (!mUsers.ContainsKey(id))
            {
                Dictionary<string, object> parameters = new Dictionary<string, object>();
                parameters.Add("Id", id);
                DatabaseUser user = DatabaseUser.GetByParameters<DatabaseUser>(mDatabaseManagerServer, parameters);
                if (user != null)
                {
                    mUsers.Add(id, user);
                }
                else
                {
                    return null;
                }
            }
            return mUsers[id];
        }

        public DatabaseUser GetDatabaseUser(string userName)
        {
            if (!mUsers.Any((user) => { return user.Value.Name.Equals(userName, StringComparison.InvariantCultureIgnoreCase); }))
            {
                Dictionary<string, object> parameters = new Dictionary<string, object>();
                parameters.Add("Name", userName);
                DatabaseUser user = DatabaseUser.GetByParameters<DatabaseUser>(mDatabaseManagerServer, parameters);
                if (user != null)
                {
                    mUsers.Add(user.Id, user);
                }
                else
                {
                    return null;
                }
            }
            return mUsers.First((user) => { return user.Value.Name.Equals(userName, StringComparison.InvariantCultureIgnoreCase); }).Value;
        }

        private DatabaseUser AuthenticateDatabaseUser(Authentication authentication)
        {
            if (authentication == null) throw new ArgumentNullException("authentication");
            if (authentication.IPAddress == null) throw new ArgumentNullException("authentication.IPAddress");
            if (authentication.Password == null) throw new ArgumentNullException("authentication.Password");
            if (authentication.Username == null) throw new ArgumentNullException("authentication.Username");
            if (!mIVUsers.Any((ipLink) => { return authentication.IPAddress.ToString().Equals(ipLink.Key.ToString()); })) throw new InvalidOperationException(string.Format("Connection failure. Attempted to connect using {0}, but was not pre-authenticated.", authentication.IPAddress));

            IPAddress realIpAddress = mIVUsers.First((ipLink) => { return authentication.IPAddress.ToString().Equals(ipLink.Key.ToString()); }).Key;
            byte[] iv = mIVUsers[realIpAddress];
            mIVUsers.Remove(realIpAddress);

            string userName = DecryptText(authentication.Username, iv, Key);
            string password = DecryptText(authentication.Password, iv, Key);
            DatabaseUser returnUser = GetDatabaseUser(userName);
            if (returnUser != null)
            {
                byte[] encryptedPasswordArray = ConvertStringToByteArray(returnUser.GetCustomField(mDatabaseManagerServer, "Password").ToString());
                string passwordOnFile = DecryptText(encryptedPasswordArray, UserIV, UserKey);
                if (passwordOnFile.Equals(password))
                {
                    returnUser.AuthenticationKey = EncryptString(DateTime.Now.ToString(), iv, Key);
                    return returnUser;
                }
                return null;
            }
            return null;
        }

        private DatabaseUser CreateDatabaseUser(Authentication authentication)
        {
            if (authentication == null) throw new ArgumentNullException("authentication");
            if (authentication.IPAddress == null) throw new ArgumentNullException("authentication.IPAddress");
            if (authentication.Password == null) throw new ArgumentNullException("authentication.Password");
            if (authentication.Username == null) throw new ArgumentNullException("authentication.Username");
            if (!mIVUsers.Any((ipLink) => { return authentication.IPAddress.ToString().Equals(ipLink.Key.ToString()); })) throw new InvalidOperationException(string.Format("Connection failure. Attempted to connect using {0}, but was not pre-authenticated.", authentication.IPAddress));

            IPAddress realIpAddress = mIVUsers.First((ipLink) => { return authentication.IPAddress.ToString().Equals(ipLink.Key.ToString()); }).Key;
            byte[] iv = mIVUsers[realIpAddress];
            mIVUsers.Remove(realIpAddress);

            string userName = DecryptText(authentication.Username, iv, Key);
            string password = DecryptText(authentication.Password, iv, Key);
            DatabaseUser returnUser = GetDatabaseUser(userName);
            if (returnUser == null)
            {
                returnUser = new DatabaseUser()
                {
                    Name = userName
                };
                returnUser.Create(mDatabaseManagerServer);
                returnUser.SetCustomField(mDatabaseManagerServer, "Password", ConvertByteArrayToString(EncryptString(password, UserIV, UserKey)), true);
                mUsers.Add(returnUser.Id, returnUser);
                returnUser.AuthenticationKey = EncryptString(DateTime.Now.ToString(), iv, Key);
                return returnUser;
            }
            return null;
        }

        private string DecryptText(byte[] cipherArray, byte[] iv, byte[] key)
        {
            RijndaelManaged cryptographer = null;
            string decryptedText = null;
            try
            {
                cryptographer = new RijndaelManaged()
                    {
                        IV = iv,
                        Key = key
                    };
                ICryptoTransform decryptor = cryptographer.CreateDecryptor();
                using (MemoryStream memoryStream = new MemoryStream(cipherArray, false))
                {
                    using (CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
                    {
                        using (StreamReader streamReader = new StreamReader(cryptoStream))
                        {
                            decryptedText = streamReader.ReadToEnd();
                        }
                    }
                }
            }
            finally
            {
                if (cryptographer == null)
                {
                    cryptographer.Clear();
                }
            }
            return decryptedText;
        }

        private byte[] ConvertStringToByteArray(string byteArray)
        {
            List<byte> bytes = new List<byte>();
            foreach (string singleByte in byteArray.Split(','))
            {
                bytes.Add(byte.Parse(singleByte));
            }
            return bytes.ToArray();
        }

        private byte[] EncryptString(string encrypt, byte[] iv, byte[] key)
        {
            RijndaelManaged authenticator = new RijndaelManaged();
            authenticator.IV = iv;
            authenticator.Key = key;
            MemoryStream memoryStream = new MemoryStream();
            try
            {
                ICryptoTransform encryptor = authenticator.CreateEncryptor();
                using (CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
                {
                    using (StreamWriter streamWriter = new StreamWriter(cryptoStream))
                    {
                        streamWriter.Write(encrypt);
                    }
                }
            }
            finally
            {
                authenticator.Clear();
            }
            return memoryStream.ToArray();
        }

        private string ConvertByteArrayToString(byte[] byteArray)
        {
            StringBuilder builder = new StringBuilder();
            foreach (byte singleByte in byteArray)
            {
                if (builder.Length > 0)
                {
                    builder.Append(",");
                }
                builder.Append(singleByte);
            }
            return builder.ToString();
        }

        private bool KeysMatch(byte[] incoming, byte[] checker)
        {
            for (int i = 0; i < incoming.Length; i++)
            {
                if (incoming[i] != checker[i])
                {
                    return false;
                }
            }
            return true;
        }


        #region IObjectServer Members

        public SerializableTransferObject CreateObject(SerializableTransferObject authorizedUser, SerializableTransferObject newObject)
        {
            #warning Not Implemented
            throw new NotImplementedException();
        }

        public void EditObject(SerializableTransferObject authorizedUser, SerializableTransferObject editObject)
        {
            #warning Not Implemented
        }

        public void DeleteObject(SerializableTransferObject authorizedUser, SerializableTransferObject deleteObject)
        {
            #warning Not Implemented
        }

        public SerializableTransferObject GetObject(SerializableTransferObject authorizedUser, string type, Dictionary<string, object> parameters)
        {
            #warning Not Implemented
            throw new NotImplementedException();
        }

        public IEnumerable<SerializableTransferObject> GetObjects(SerializableTransferObject authorizedUser, string type, Dictionary<string, object> parameters)
        {
            #warning Not Implemented
            throw new NotImplementedException();
        }

        #endregion
    }
}
